package serial

import (
	"context"
	"errors"
	"fmt"
	"math"
	"strings"
	"sync"
	"time"

	"github.com/project-flogo/core/app"
	"github.com/project-flogo/core/data/metadata"

	serial "gitee.com/shipnet/xserial-go"
	"github.com/project-flogo/core/support/log"
	"github.com/project-flogo/core/trigger"
)

func init() {
	_ = trigger.Register(&Trigger{}, &Factory{})
}

type Trigger struct {
	glock    *sync.Mutex
	device   *serial.Serial
	logger   log.Logger
	settings serial.SerialSettings
	stopC    chan int
	handlers []trigger.Handler
}

type Factory struct{}

var triggerMd = trigger.NewMetadata(&Settings{}, &HandlerSettings{}, &Output{}, &Reply{})

func (f *Factory) Metadata() *trigger.Metadata {
	return triggerMd
}

func (f *Factory) New(config *trigger.Config) (trigger.Trigger, error) {
	var settings Settings
	err := metadata.MapToStruct(config.Settings, &settings, true)
	if err != nil {
		return nil, err
	}
	stopBits, err := toStopBits(settings.StopBits)
	if err != nil {
		return nil, err
	}
	parity, err := toParity(settings.Parity)
	if err != nil {
		return nil, err
	}
	readTimeout := time.Second * 1
	if strings.TrimSpace(settings.ReadTimeout) != "" {
		readTimeout, err = time.ParseDuration(settings.ReadTimeout)
		if err != nil {
			return nil, err
		}
	}
	trigger := Trigger{
		settings: serial.SerialSettings{
			Device:      settings.Port,
			Baudrate:    toBaudrate(settings.Baudrate),
			DataBits:    toDataBits(settings.DataBits),
			StopBits:    stopBits,
			Parity:      parity,
			ReadTimeout: readTimeout,
		},
	}
	return &trigger, nil
}

func toStopBits(st float32) (serial.StopBitsType, error) {
	if st == 0 {
		return serial.Stop1, nil
	} else if st == 1 {
		return serial.Stop1, nil
	} else if st == 2 {
		return serial.Stop2, nil
	}
	e := math.Abs(float64(st - 1.5))
	if e < 0.01 {
		return serial.Stop1p5, nil
	} else {
		return serial.Stop1, fmt.Errorf("invalid stop bits: %f", st)
	}
}

var parityMap = map[string]serial.Parity{
	"NONE":  serial.ParityNone,
	"ODD":   serial.ParityOdd,
	"EVEN":  serial.ParityEven,
	"MARK":  serial.ParityMark,
	"SPACE": serial.ParitySpace,
}

func toParity(p string) (serial.Parity, error) {
	trimd := strings.TrimSpace(p)
	if trimd == "" {
		return serial.ParityNone, nil
	}
	pt := strings.ToUpper(trimd)
	if ps, ok := parityMap[pt]; ok {
		return ps, nil
	} else {
		return serial.ParityNone, fmt.Errorf("invalid parity: %s", p)
	}
}

func toDataBits(d int) int {
	if d <= 0 {
		return 8
	}
	return d
}

func toBaudrate(d int) int {
	if d <= 0 {
		return 115200
	}
	return d
}

func (t *Trigger) Initialize(ctx trigger.InitContext) error {
	t.logger = ctx.Logger()
	// settings := t.settings
	lock, err := getOrCreateGlobalLock()
	if err != nil {
		return err
	}
	t.glock = lock
	t.handlers = ctx.GetHandlers()
	t.stopC = make(chan int)
	return nil
}

func getOrCreateGlobalLock() (*sync.Mutex, error) {
	name := "serialGlobalLock"
	if s, ok := app.GetValue(name); ok {
		sl, y := s.(*sync.Mutex)
		if y {
			return sl, nil
		} else {
			return nil, errors.New("invalid serial global lock")
		}
	} else {
		s := sync.Mutex{}
		err := app.SetValue(name, &s)
		if err != nil {
			return nil, err
		}
		return &s, nil
	}
}

func (t *Trigger) Start() error {
	sd, err := t.getOrCreateSerial(t.settings)
	if err != nil {
		return err
	}
	sd.Ref()
	err = sd.StartRead()
	if err != nil {
		return err
	}
	t.device = sd
	go t.run()
	return nil
}

func (t *Trigger) getOrCreateSerial(settings serial.SerialSettings) (*serial.Serial, error) {
	t.glock.Lock()
	defer t.glock.Unlock()
	key := "serial/" + settings.Device
	s, ok := app.GetValue(key)
	if !ok {
		// create new instance
		sd, err := serial.NewSerial(settings)
		if err != nil {
			return nil, err
		}
		err = app.SetValue(key, sd)
		if err != nil {
			return nil, err
		}
		return sd, nil
	} else {
		switch sd := s.(type) {
		case *serial.Serial:
			{
				return sd, nil
			}
		default:
			{
				return nil, errors.New("not a serial instance")
			}
		}
	}
}

func (t *Trigger) Stop() error {
	t.stopC <- 1
	if t.device != nil {
		err := t.device.Stop()
		return err
	}
	return nil
}

func (t *Trigger) run() {
	for {
		select {
		case <-t.stopC:
			t.logger.Debugf("trigger exit")
			return
		case data := <-t.device.DataC():
			t.runHandler(data, nil)
		case err := <-t.device.ErrC():
			t.runHandler(nil, err)
		}
	}
}

func (t *Trigger) runHandler(buf []byte, err error) {
	errStr := ""
	if err != nil {
		errStr = err.Error()
	}
	for _, handler := range t.handlers {
		output := &Output{
			Data: buf,
			Err:  errStr,
		}
		_, err := handler.Handle(context.Background(), output)
		if err != nil {
			t.logger.Errorf("failed running action: %s", err.Error())
		}
	}
}
